Accustoming Yourself to CPP
本书的更新截止至 2005 年
导读部分提到了 Declaration 和 Definition 的区别,谈论的对象包括 Object, Function, Class, Template。
提到了函数的 Signature。
提到了 Initialization,解释了什么是 Default Constructor,即可以不传入任何参数的形式被调用的构造函数。
提到了 Implicit Type Conversions 和 Explicit Type Conversions 以及 explicit 在声明构造函数时的作用。
提到了 Copy Constructor 和 Copy Assignment Operator 的区别。提到 Copy Constructor 在函数 Pass-by-Value 传参中被采用。
MyClass var1 (var2);
MyClass var1 = var2; // 调用复制构造函数
var1 = var3; // 调用赋值运算符
提到了两种 Undefined Behaviour:对空指针解引用,数组下标越界。
View C++ as a Federation of Languages
C++ 并不仅仅是一个 C with classes,而是一个 Multiparadigm programming language。同时支持
- 过程形式
- 面对对象形式
- 函数形式
- 泛型形式
- 元编程形式
语言联邦的主要次语言有 - C
- Object-Oriented C++
- Template C++
- STL
不同次语言的守则可能不同,当在次语言切换时适应守则的变化。
Prefer consts, enums and inlines to #defines
说的是常量的处理。本质上是以编译器替代预处理器。#define 并不被视为语言的一部分
- 其定义的 Names 并未进入编译器的 Symbol Table。
- 替代产生的常量会导致常量在 Object Code(.o file) 中出现多次。
- 无法实现作用域特性
常量可分为全局常量、命名空间内常量、类内静态常量、函数内静态常量。后两者添加静态修饰为了是防止(类在创建实例时,函数在调用时)出现多份。前三者如果定义在头文件中会导致导入其的源文件各自有一份该常量。前三者可以在头文件中 extern 加以避免(注意类内静态常量在定义时形式的特殊性),但需要在源文件中进行定义。
特殊地:
char*-based 字符串的常量(常量指针)声明需要两个const,更推荐std::string。- 类内静态常量的初始值声明。Const#Initialize Static Variables in Classes
- 将类内静态常量的值放在类内的形式相较于在定义式中指定值而言的优势是:可以实现编译期的常量,如在指定数组长度时。
- enum hack 是“将类内静态常量的值放在类内的形式”的等效做法。enum 又类似于
#define因为它无法被取地址(不会为这个常量作为一个变量地分配地址、这个被直接替换的值作为 literal 地拥有地址)
用 #define 实现宏的缺点(全是因为它直接替换的原因)
- 小括号
- 当参数带有 side effect 时行为变异
- 不重视作用域
Use const whenever possible
const 的含义是指定一个“不应该发生改变的”语义约束,并驱使编译器为你保证这项约束。
const 拥有多种形式,常见于指针的多重 const 含义;也可以作用于各种地方:
- namespace/local scope 中的变量
- class 成员变量
const MyClass var; 和 MyClass const var; 的写法是相同的。
函数的返回值是右值时,如果需要,使用 const type 而不是 type 可以避免对该右值进行赋值等带有改变性质的操作(通常是无意的,如 == 写成 =),因为通常右值没有 storage。
成员函数函数本身是 const 函数使得其可以作用于 const对象。
- 可以一定程度上解释接口的作用是否会对对象产生影响
两个成员函数如果只有 constness 不同那么是可以被重载的,const对象只能调用const函数,而 non-const 对象都可以调用,并且优先调用 non-const 版本。
bitwise constness 和 logical constness 的争议
这里考虑的是成员函数的 constness。
bitwise 是说该对象操作物理上不改变该对象自身的任何一个 bit,C++ 的 const 采用的便是这种规定
- 优点是便于侦察违反点
- 缺点是许多例外能通过 bitwise 测试,如对指针指向物的改变不予考虑,如返回一个非 const 引用。这在 bitwise 上是合理的,但是却不符合逻辑。可见 bitwise constness 的不灵活性。
logical 是说在客户端侦测不出来的情况下可以修改,也就是说从用户的视角看顶层,它是满足 constness 的,但是底层实现的许多地方需要非 const 操作,而且这些操作不影响顶层表征的 constness。如 CTextBlock 可能 cache 文本长度应付查询。这是应当在实际编程中采用的。
那么就应当有一种特性使得这些非 bitwise constness 的底层操作可以通过编译器的 constness 检查,mutable 修饰符应运而生。
避免重复
有时同时需要 const 和 non-const 两个版本,如 operator[]。const 和 non-const 成员函数的行为通常十分相似,重复一遍会造成代码膨胀,增加编译时间、维护成本。解决方法是让 non-const 版本调用 const 版本,重要的实现手段是 const_cast<>()。
Make sure that objects are initialized before they're used
不同语言体系中的标准不同:C part of C++ 中不保证发生初始化,如各种内置类型;但是 non-C part of C++ 中保证(由构造函数等来实现)。实际编程中应该采用 non-C part of C++ 的标准:对于内置类型进行手动初始化,对于用户自定义类型,编写良好的构造函数并进行良好的初始化。
读取未初始化的值是 UB。
不要混淆赋值和初始化。
初始化有两种手段:member initializer list 和 default initializer。后者存在的目的是减少在不同构造函数中的重复工作,在前者中经初始化的对象不会再经后者。
单个类成员初始化次序并不被 member initializer list 中的次序决定。
1) If the constructor is for the most-derived class, virtual bases are initialized in the order in which they appear in depth-first left-to-right traversal of the base class declarations (left-to-right refers to the appearance in base-specifier lists).
2) Then, direct bases are initialized in left-to-right order as they appear in this class's base-specifier list.
3) Then, non-static data member are initialized in order of declaration in the class definition.
4) Finally, the body of the constructor is executed.
不同编译单元(产出目标文件的 .cpp 加上 #include 的头文件)之间初始化次序有依赖的 non-local static 对象的初始化次序。C++ 标准对此是无定义的,原因包括决定次序的困难性:多个编译单元内 non-local static 对象经由 implicit templatae instantiations 形成。解决方法是使用 Singleton 模式中常见的用 local static 代替 global static,缺点是在多线程时进行初始化不够安全;没有显式指定初始化顺序的清晰性。